{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "LmfLXp5_bt-a",
    "tags": []
   },
   "source": [
    "# 知识库问答Demo说明文档"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "bbPzgYbrwbK2"
   },
   "source": [
    "本示例展示如何利用千帆API与文心大模型构建基础的知识库问答系统。\n",
    "\n",
    "系统包含两个主要环节：知识库构建与基于知识库的问答，通过文本向量化与向量检索技术的结合，实现基于知识库的准确回答。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "IS2GUSP0_Eim"
   },
   "source": [
    "## 1. 环境准备\n",
    "在使用本demo前，需要准备好以下环境：\n",
    "- 安装 Python 3.10 至 3.12 版本环境。\n",
    "- 安装必要的Python库：`openai`、`hashlib`、`json`、`numpy`、`textwrap`、`faiss`\n",
    "- 部署[ERNIE-4.5](https://github.com/PaddlePaddle/FastDeploy)系列模型服务并正确配置对应的服务地址`host_url`\n",
    "- 设置向量模型相关参数，包括向量模型地址`embedding_service_url`、模型名称`embedding_model`、密钥`qianfan_api_key`\n",
    "    - 关于千帆支持的向量模型，请参阅[千帆向量模型](https://cloud.baidu.com/doc/qianfan-docs/s/Um8r1tpwy)。确定`embedding_model`后需要根据模型设置对应的`embedding_dim`\n",
    "    - 您可以登录[千帆](https://console.bce.baidu.com/iam/#/iam/apikey/list)创建您的API密钥。API密钥是敏感信息，应妥善保管\n",
    "- 设置测试用的文本文件`test_file`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "id": "YD6urJjWGVDf"
   },
   "outputs": [],
   "source": [
    "# 模型服务地址配置\n",
    "host_url = \"http://localhost:port/v1\"\n",
    "\n",
    "# 千帆 API 配置\n",
    "embedding_service_url = \"https://qianfan.baidubce.com/v2\"\n",
    "qianfan_api_key = \"bce-v3/xxx\"  # 替换为您的真实API密钥\n",
    "embedding_model = \"embedding-v1\"\n",
    "embedding_dim = 384  # 嵌入维度，根据模型调整\n",
    "top_k = 3  # 单个问题检索返回的文本数量\n",
    "model_api_key = \"api_key\" # 请求模型的API密钥，使用本地部署的模型时可忽略使用任意值.\n",
    "\n",
    "# 测试文件\n",
    "test_file = \"../data/coffee.txt\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1. 安装依赖项"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install openai numpy faiss-cpu"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 示例实现流程\n",
    "\n",
    "- **知识库构建**：在开发知识库问答系统时，构建向量知识库是核心前置工作。主要包括两个步骤。\n",
    "\n",
    "    1) **文本分割处理**：首先对原始文档进行分块处理，将长文档转化为适合语义检索的知识单元。\n",
    "\n",
    "    2) **文本块向量化处理及灌库**：通过嵌入模型将文本块转化为高维向量。建立原始文本与向量数据的关联存储，形成高效检索的\"文本-向量\"双索引结构存储在知识库中。\n",
    "    \n",
    "    知识库的构建使得系统能够通过计算问题向量与知识库向量的相似度，快速定位最相关的文本片段，为大模型生成精准回答提供事实依据，有效解决了模型可能存在的\"幻觉\"问题。\n",
    "\n",
    "- **基于知识库的问答**：使用文心大模型的 API 来创建一个知识库问答系统。问答过程中主要包括以下三个步骤。\n",
    "\n",
    "    1) **检索问题改写**：首先分析用户问题是否需要从知识库中检索文本，需要检索时提炼出用于检索的子问题。\n",
    "\n",
    "    2) **知识库检索**：为问题生成向量，将其与知识库中存储的文本向量进行比较，得到相关文本段落。\n",
    "\n",
    "    3) **生成最终回答**：基于检索回来的相关文本段落整理检索结果，使用大模型生成最终的回答。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "dD1lQx3Zr3S2"
   },
   "source": [
    "## 3. 知识库构建\n",
    "\n",
    "本示例使用`faiss`数据库来存储和检索向量。\n",
    "\n",
    "### 3.1. 文本分割处理\n",
    "\n",
    "假设有一份关于咖啡的文档，你需要为这份文档构建一个向量（embeddings）数据库，以便之后可以根据查询query检索相关信息。\n",
    "\n",
    "在将文本向量化之前，需要先将文本分块。本示例将`chunk_size`设置为512，采用按行切分的策略以便细粒度的检索到更相关的文本片段。\n",
    "- 如果一行的内容不足512，则增加下一行内容；如果超过512，则寻找最近标点将行内容截断。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "id": "XvLRIbpq4vNN"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，', '公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。', '咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。', '同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。', '18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。']\n"
     ]
    }
   ],
   "source": [
    "def split_oversized_line(line: str, chunk_size: int) -> tuple:\n",
    "    PUNCTUATIONS = {\".\", \"。\", \"!\", \"！\", \"?\", \"？\", \",\", \"，\", \";\", \"；\", \":\", \"：\"}\n",
    "\n",
    "    if len(line) <= chunk_size:\n",
    "        return line, \"\"\n",
    "\n",
    "    # 从 chunk_size 位置向后搜索\n",
    "    split_pos = chunk_size\n",
    "    for i in range(chunk_size, 0, -1):\n",
    "        if line[i] in PUNCTUATIONS:\n",
    "            split_pos = i + 1\n",
    "            break\n",
    "\n",
    "    # 如果没找到标点符号，回退到空格分割\n",
    "    if split_pos == chunk_size:\n",
    "        split_pos = line.rfind(\" \", 0, chunk_size)\n",
    "        if split_pos == -1:\n",
    "            split_pos = chunk_size\n",
    "\n",
    "    return line[:split_pos], line[split_pos:]\n",
    "\n",
    "def split_text_into_chunks(text: str, chunk_size: int) -> list:\n",
    "    lines = [line.strip() for line in text.split(\"\\n\") if line.strip()]\n",
    "    chunks = []\n",
    "    current_chunk = []\n",
    "    current_length = 0\n",
    "\n",
    "    for line in lines:\n",
    "\n",
    "        # 如果添加line会超过chunk大小限制（且当前块非空）\n",
    "        if current_length + len(line) > chunk_size and current_chunk:\n",
    "            chunks.append(\"\\n\".join(current_chunk))\n",
    "            current_chunk = []\n",
    "            current_length = 0\n",
    "\n",
    "        # 分割过长的文本\n",
    "        while len(line) > chunk_size:\n",
    "            head, line = split_oversized_line(line, chunk_size)\n",
    "            chunks.append(head)\n",
    "\n",
    "        # 添加剩余文本内容\n",
    "        if line:\n",
    "            current_chunk.append(line)\n",
    "            current_length += len(line) + 1\n",
    "\n",
    "    if current_chunk:\n",
    "        chunks.append(\"\\n\".join(current_chunk))\n",
    "    return chunks\n",
    "\n",
    "with open(test_file, \"r\", encoding=\"utf-8\") as f:\n",
    "    test_text = f.read()\n",
    "\n",
    "segments = split_text_into_chunks(test_text, chunk_size=512)\n",
    "print(segments[:5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "LHonPYEwStLB"
   },
   "source": [
    "### 3.2. 文本块向量化及灌库\n",
    "\n",
    "#### 3.2.1. 向量生成示例\n",
    "\n",
    "本示例使用千帆 API 生成文本的向量（embedding）。\n",
    "\n",
    "向量是一种文本的向量化表示，即由一组浮点数组成的数组。这种表示方式在各种自然语言处理任务中非常有用。在本示例中，我们将使用向量来计算查询与知识库中的文本之间的相似度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[0.06902910023927689, -0.02064981311559677, -0.09815074503421783, 0.08861374109983444, -0.05157714709639549, -0.13549675047397614, 0.027453964576125145, -0.023358309641480446, -0.026020705699920654, -0.01652238890528679, -0.0017636652337387204, -0.08663472533226013, 0.060991719365119934, -0.001433665631338954, -0.07765280455350876, 0.010916685685515404, 0.0037388403434306383, -0.008097093552350998, -0.004761330783367157, -0.03509196266531944, 0.025797778740525246, -0.053582921624183655, 0.0033174229320138693, -0.04033001884818077, 0.03428077697753906, 0.006707459222525358, 0.05069943889975548, 0.08020070195198059, 0.02807926945388317, 0.012830601073801517, 0.005108212120831013, 0.0682472214102745, 0.08067227900028229, -0.08299519866704941, -0.041390180587768555, 0.06293655186891556, -0.022655190899968147, 0.000996474758721888, -0.026314564049243927, 0.08683788776397705, 0.02757883071899414, -0.0027709805872291327, 0.028281809762120247, -0.018270548433065414, 0.08003634959459305, -0.12196885049343109, -0.056998468935489655, -0.02333405241370201, -0.06551925092935562, 0.0315762422978878, 0.001037031877785921, 0.04430923983454704, 0.1054653599858284, 0.06728596985340118, -0.01829889416694641, -0.008648361079394817, -0.07413625717163086, -0.026104599237442017, 0.059346046298742294, -0.05114193260669708, -0.04633801430463791, 0.0364503338932991, 0.10136435180902481, -0.048290807753801346, 0.10299105942249298, 0.05649077519774437, -0.03151465207338333, 0.022651713341474533, 0.04111502692103386, -0.009610558860003948, 0.03487059473991394, -0.05934298038482666, 0.02765224501490593, 0.034416649490594864, -0.0202739629894495, 0.08460138738155365, -0.0083199767395854, 0.023836616426706314, 0.06835906207561493, -0.07559925317764282, 0.08442452549934387, 0.006683662533760071, 0.08673819154500961, 0.015553551726043224, -0.11905435472726822, -0.04445292428135872, 0.10648924112319946, 0.007692687679082155, -0.0026310172397643328, -0.027316149324178696, 0.011222605593502522, -0.08177153021097183, 0.07688300311565399, -0.012517046183347702, -0.10624496638774872, -0.07089240849018097, -0.04570743441581726, 0.026570936664938927, 0.0055390591733157635, -0.10976123809814453, 0.007593727204948664, 0.0798032358288765, -0.004682272672653198, 0.002999532036483288, 0.09999089688062668, -0.05949293449521065, -0.0798720270395279, -0.07874537259340286, 0.02925257571041584, 0.012559361755847931, -0.08104752749204636, 0.04633769765496254, -0.05368822440505028, 0.009231893345713615, 0.008213266730308533, 0.04279320687055588, 0.011830301955342293, -0.022767391055822372, 0.009919494390487671, 0.00734842661768198, -0.030985893681645393, 0.02011285535991192, -0.05481714755296707, 0.07407107204198837, -0.07009966671466827, 0.04820315167307854, -0.14042289555072784, -0.0025907987728714943, 0.008534450083971024, 0.06085103005170822, 0.08107241988182068, 0.051909416913986206, -0.04352433979511261, 0.037940531969070435, 0.02488063834607601, 0.0511770024895668, -0.018011318519711494, -0.06472807377576828, -0.05579017475247383, 0.07917359471321106, -0.05891445279121399, 0.08271721750497818, 0.04542738199234009, 0.00040362621075473726, -0.03726900741457939, 0.020076602697372437, 0.025510020554065704, -0.01853681169450283, -0.03148650750517845, -0.044669125229120255, -0.08029467612504959, 0.061398573219776154, -0.044040605425834656, -0.03548990190029144, -0.0766347348690033, 0.001127738389186561, 0.1083831936120987, -0.11496994644403458, -0.06664140522480011, 0.03932565823197365, 0.023469578474760056, -0.02688688039779663, 0.07108504325151443, -0.04895724728703499, 0.022105012089014053, 0.00519839022308588, -0.010209089145064354, 0.036910876631736755, 0.05414630100131035, 0.05123646557331085, -0.06635504215955734, 0.06486642360687256, 0.02087211050093174, -0.06921166926622391, 0.04187197610735893, 0.026369638741016388, 0.04336056485772133, 0.044106848537921906, 0.04993714019656181, -0.01616659201681614, -0.04925931990146637, -0.03643150255084038, 0.0492049939930439, -0.07763552665710449, 0.040213581174612045, -0.005090398248285055, -0.05563517287373543, 0.08514751493930817, 0.015355469658970833, -0.10387051850557327, 0.018668709322810173, 0.07077160477638245, -0.027486123144626617, -0.014717619866132736, 0.004695506766438484, -0.012303460389375687, -0.004724535625427961, -0.05144288018345833, -0.03640333190560341, 0.037046756595373154, -0.008569135330617428, 0.010247169993817806, -0.004085906315594912, -0.07353091239929199, 0.022071469575166702, -0.030118674039840698, -0.10056933015584946, 0.014777586795389652, -0.0227784663438797, 0.007048485334962606, -0.0014997408725321293, 0.023030906915664673, -0.01078913826495409, -0.07683145254850388, 0.019512755796313286, 0.09775645285844803, 0.03273649886250496, -0.036535054445266724, 0.17036518454551697, 0.034777067601680756, -0.057165149599313736, 0.011068180203437805, 0.1110801175236702, -0.07050687819719315, 0.06646030396223068, 0.03532341867685318, -0.0036992349196225405, -0.01897468790411949, -0.0027495413087308407, -0.06805577874183655, -0.012349153868854046, -0.03379824757575989, 0.04108939692378044, 0.10483039915561676, -0.024189557880163193, -0.02803787775337696, -0.08627132326364517, -0.06382598727941513, 0.06018282100558281, -0.06542231142520905, -0.003932671621441841, -0.030126821249723434, 1.0333528734918218e-05, -0.03628602251410484, 0.00844994280487299, -0.01794985681772232, 0.08037517964839935, 0.08628460019826889, -0.0351770743727684, 0.026639962568879128, -0.05487348511815071, -0.009014436975121498, -0.011714656837284565, 0.046275921165943146, 0.005093749612569809, -0.08301075547933578, 0.0, 0.0, -0.0005039895768277347, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0027954732067883015, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.3417782485485077, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.35276928544044495, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]\n"
     ]
    }
   ],
   "source": [
    "from openai import OpenAI\n",
    "\n",
    "\n",
    "def embed_fn(text):\n",
    "    client = OpenAI(base_url=embedding_service_url, api_key=qianfan_api_key)\n",
    "    response = client.embeddings.create(input=[text], model=embedding_model)\n",
    "    return response.data[0].embedding\n",
    "\n",
    "print(embed_fn(\"hello, world!\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.2.2. 文本块向量化及灌库\n",
    "\n",
    "为文件的每段文本生成向量（embedding），并将向量信息添加到`faiss`数据库中，文本信息存储到`.jsonl`文件中。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "id": "4SOhy0lNBhfN"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "embedding:\n",
      "[[ 0.15310541  0.04237456  0.12356884 ...  0.02131988  0.01291032\n",
      "  -0.00546286]\n",
      " [ 0.11144318  0.09504584  0.0567933  ...  0.          0.\n",
      "   0.        ]\n",
      " [ 0.1457673   0.10907461  0.08814128 ...  0.02718093  0.\n",
      "   0.03284565]\n",
      " ...\n",
      " [ 0.10487412 -0.02126087  0.08551556 ...  0.          0.\n",
      "   0.        ]\n",
      " [ 0.13665409  0.0698209   0.04600666 ... -0.03795466  0.\n",
      "   0.        ]\n",
      " [ 0.08973876 -0.02730799  0.00867874 ... -0.0553185   0.\n",
      "   0.04129887]]\n",
      "-------------------------------------------------------------------\n",
      "text_db:\n",
      "{'file_md5s': ['338f7fd3003e6f1d59f8ee92739ed88d'], 'chunks': [{'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，', 'vector_id': 0}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。', 'vector_id': 1}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。', 'vector_id': 2}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。', 'vector_id': 3}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。', 'vector_id': 4}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\\n不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：', 'vector_id': 5}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\\n浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\\n拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。', 'vector_id': 6}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '卡布奇诺：卡布奇诺是一种意大利咖啡，是由在浓缩咖啡上倒入奶泡制成，由于咖啡的颜色就像方济嘉布遣会修士深褐色外衣上覆的头巾一样，卡布奇诺也因此得名。其与拿铁咖啡类似，区别仅是卡布奇诺在咖啡、牛奶、奶泡的比例为1:1:1。卡布奇诺咖啡奶泡多，而拿铁咖啡的奶泡少。口味上卡布奇诺咖啡的咖啡味重，而拿铁较为清淡一些，这是因为拿铁的牛奶更多。\\n摩卡咖啡：通常是由三分之一的意式浓缩咖啡和三分之二的奶泡配成，并加入少量巧克力糖浆或速溶巧克力粉。拉夫咖啡是在单杯浓缩咖啡中添加带有少量泡沫（0.5 厘米）的奶油而制成的咖啡。通常与香草糖一起喝用但通常使用糖浆代替香草糖。\\n玛琪雅朵咖啡：在冲泡好的浓缩咖啡上加入鲜奶并倒入一层较薄的奶泡的意大利咖啡。\\n焦糖玛琪雅朵：是一种在浓缩咖啡加入热牛奶和香草，最后淋上焦糖制成的玛琪雅朵咖啡。\\n欧蕾咖啡：是一种咖啡和牛奶的比例为1:1的牛奶咖啡，在冲泡时，需要牛奶壶和咖啡壶从两旁同时注入到咖啡杯。在星巴克则被称为Caffè Misto，以1:1比例的法式压滤咖啡搭配奶泡而成。', 'vector_id': 7}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '美式咖啡：是一种浓缩咖啡以1:5比例加入热水稀释制成的咖啡饮料。冲泡美式咖啡亦可使用意式咖啡机萃取浓缩咖啡，而在咖啡萃取完成后，继续使用咖啡机向浓缩咖啡加入热水稀释到合适比例即可。其浓度随浓缩咖啡的冲泡次数和添加的水量而变化，美式咖啡具有浓缩咖啡风味但却更为柔和。\\n长黑咖啡：是澳大利亚和新西兰常见的一种咖啡，是将双份浓缩咖啡倒入热水中制成的，其恰好与美式咖啡截然相反。长黑咖啡通常使用约100–120毫升的水，但水量可根据个人口味灵活调整。\\n维也纳咖啡：其制作方式为将糖或粗砂糖放入杯内再倒入热咖啡，杯上挤入鲜奶油以及巧克力膏，最终撒上彩色糖粒装饰即可。此种制法可追溯至1683年，当时乌克兰裔波兰军官耶日·弗朗西泽克·库奇茨基开设了奥地利首家咖啡馆并在维也纳开业，其普及了在咖啡中加糖和牛奶的制作和饮用方式。而维也纳咖啡传说是由奥地利马车夫爱因·舒伯纳发明。\\n爱尔兰咖啡：在咖啡中加入威士忌后在其顶部放上奶油。而加入威士忌的爱尔兰咖啡能将咖啡的酸甜味衬托出来。\\n调味咖啡：依据口味的不同在咖啡中加入巧克力、糖浆、果汁、肉桂、肉豆蔻、橘子花等不同调味料。', 'vector_id': 8}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '康宝蓝：康宝蓝是一种在意大利浓缩咖啡上倒入适量奶油的咖啡，并用玻璃咖啡杯盛装，由于鲜奶油具有甜味因此通常无需加糖。\\n白咖啡：起源于马来西亚怡保，其使用经过人造黄油烘培的咖啡豆，冲泡好后加入甜炼乳的饮品。19世纪和20世纪初英国锡矿公司在怡保设立锡矿，而中国移民则在怡保锡矿工作，白咖啡是19世纪中后期移民马来亚的海南人出于华人不习惯咖啡味道而发明。从本质上是一种拿铁咖啡。在美国，白咖啡也指轻度烘培的咖啡豆，使用意式冲煮，具有较强酸味的咖啡。\\n越南咖啡：是一种滴漏咖啡，冲泡时先在盛装咖啡的杯子中倒入炼乳，将滴漏壶置于盛装的杯上，并向滴漏壶加入咖啡末，再以压板压住咖啡末，倒入热水后等待滴漏。越南常用的咖啡豆品种为罗布斯塔，因其带有较重的酸味与苦味以及烘焙时间较长，使得风味较重，因此需要加入炼乳饮用。\\n印度滴漏咖啡：其通常是阿拉比卡咖啡或咖啡公豆制作的；咖啡豆经过深度烘焙、研磨并与菊苣混合，咖啡占混合物的80-90%，其余的为菊苣。菊苣的轻微苦味有助于产生印度滴漏咖啡的风味，传统上使用粗糖或蜂蜜作为甜味剂，但自1900年代中期改为白糖。', 'vector_id': 9}, {'file_md5': '338f7fd3003e6f1d59f8ee92739ed88d', 'text': '皇家咖啡：据说拿破仑在俄法战争时，因遭遇俄国酷寒的冬天，于是命令下属在咖啡里倒入白兰地取暖而发明。其制作方式为，在预热好的咖啡杯中倒入热咖啡，将咖啡匙架在杯缘上，在咖啡匙上放置方糖后淋上白兰地并点火燃烧，火焰熄灭后将咖啡匙放入咖啡搅拌至方糖溶解即可饮用。\\n黑咖啡：是使用滴滤法、渗滤法、虹吸法或加法冲泡的咖啡，在饮用时不添加牛奶、糖等调味品。速溶咖啡是不属于黑咖啡的范围的。\\n希腊法拉沛咖啡：通常由速溶咖啡、糖和牛奶制成的冰咖啡，咖啡中也会倒入奶泡；其口感微甜凉爽，适宜在夏季饮用。\\n阿芙佳朵：是种近乎甜点的冰咖啡，由冰淇淋上加入意大利浓缩咖啡制成。会加入焦糖来增加甜味和促进口感，或加入巧克力酱、可可粉、肉桂粉等。', 'vector_id': 10}]}\n"
     ]
    }
   ],
   "source": [
    "import hashlib\n",
    "import json\n",
    "\n",
    "import faiss\n",
    "import numpy as np\n",
    "\n",
    "\n",
    "def add_embeddings(file_path: str, segments: list[str]) -> bool:\n",
    "    with open(file_path, \"rb\") as f:\n",
    "        file_md5 = hashlib.md5(f.read()).hexdigest()\n",
    "    if file_md5 in text_db[\"file_md5s\"]:\n",
    "        print(f\"File already processed: {file_path} (MD5: {file_md5})\")\n",
    "        return\n",
    "\n",
    "    # 生成向量\n",
    "    vectors = []\n",
    "    for i, segment in  enumerate(segments):\n",
    "        vectors.append(embed_fn(segment))\n",
    "    vectors = np.array(vectors)\n",
    "    print(\"embedding:\")\n",
    "    print(vectors)\n",
    "    index.add(vectors.astype('float32'))\n",
    "\n",
    "    start_id = len(text_db[\"chunks\"])\n",
    "    for i, text in enumerate(segments):\n",
    "        text_db[\"chunks\"].append({\n",
    "            \"file_md5\": file_md5,\n",
    "            \"text\": text,\n",
    "            \"vector_id\": start_id + i\n",
    "        })\n",
    "\n",
    "    text_db[\"file_md5s\"].append(file_md5)\n",
    "\n",
    "index = faiss.IndexFlatIP(embedding_dim)\n",
    "text_db = {\n",
    "    \"file_md5s\": [],  # 保存file_md5s避免文件重复\n",
    "    \"chunks\": []      # 保存文本块\n",
    "}\n",
    "add_embeddings(test_file, segments)\n",
    "print(\"-------------------------------------------------------------------\")\n",
    "print(\"text_db:\")\n",
    "print(text_db)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 3.2.3. 知识库落盘"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "faiss.write_index(index, \"index.faiss\")\n",
    "with open(\"text_db.jsonl\", 'w', encoding='utf-8') as f:\n",
    "    json.dump(text_db, f, ensure_ascii=False, indent=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "ebkGT0ha5Ln3"
   },
   "source": [
    "## 4. 基于知识库的问答\n",
    "### 4.1. 检索问题改写\n",
    "判断是否需要从知识库中检索文本，若需要，则改写出待检索问题。需要准备一个提示（prompt）来引导模型完成任务，返回标准化的JSON格式结果。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "【当前时间】\n",
      "2025-06-25 14:54:36\n",
      "\n",
      "【对话内容】\n",
      "user:\n",
      "1675 年时，英格兰有多少家咖啡馆？\n",
      "\n",
      "\n",
      "你的任务是根据上面user与assistant的对话内容，理解user意图，改写user的最后一轮对话，以便更高效地从知识库查找相关知识。具体的改写要求如下：\n",
      "1. 如果user的问题包括几个小问题，请将它们分成多个单独的问题。\n",
      "2. 如果user的问题涉及到之前对话的信息，请将这些信息融入问题中，形成一个不需要上下文就可以理解的完整问题。\n",
      "3. 如果user的问题是在比较或关联多个事物时，先将其拆分为单个事物的问题，例如‘A与B比起来怎么样’，拆分为：‘A怎么样’以及‘B怎么样’。\n",
      "4. 如果user的问题中描述事物的限定词有多个，请将多个限定词拆分成单个限定词。\n",
      "5. 如果user的问题具有**时效性（需要包含当前时间信息，才能得到正确的回复）**的时候，需要将当前时间信息添加到改写的query中；否则不加入当前时间信息。\n",
      "6. 只在**确有必要**的情况下改写，不需要改写时query输出[]。输出不超过 5 个改写问题，不要为了凑满数量而输出冗余问题。\n",
      "\n",
      "【输出格式】只输出 JSON ，不要给出多余内容\n",
      "```json\n",
      "{\n",
      "\"query\": [\"改写问题1\", \"改写问题2\"...]\n",
      "}```\n",
      "\n"
     ]
    }
   ],
   "source": [
    "import textwrap\n",
    "from datetime import datetime\n",
    "\n",
    "QUERY_REWRITE_PROMPT = textwrap.dedent(\"\"\"\\\n",
    "    【当前时间】\n",
    "    {TIMESTAMP}\n",
    "\n",
    "    【对话内容】\n",
    "    {CONVERSATION}\n",
    "\n",
    "    你的任务是根据上面user与assistant的对话内容，理解user意图，改写user的最后一轮对话，以便更高效地从知识库查找相关知识。具体的改写要求如下：\n",
    "    1. 如果user的问题包括几个小问题，请将它们分成多个单独的问题。\n",
    "    2. 如果user的问题涉及到之前对话的信息，请将这些信息融入问题中，形成一个不需要上下文就可以理解的完整问题。\n",
    "    3. 如果user的问题是在比较或关联多个事物时，先将其拆分为单个事物的问题，例如‘A与B比起来怎么样’，拆分为：‘A怎么样’以及‘B怎么样’。\n",
    "    4. 如果user的问题中描述事物的限定词有多个，请将多个限定词拆分成单个限定词。\n",
    "    5. 如果user的问题具有**时效性（需要包含当前时间信息，才能得到正确的回复）**的时候，需要将当前时间信息添加到改写的query中；否则不加入当前时间信息。\n",
    "    6. 只在**确有必要**的情况下改写，不需要改写时query输出[]。输出不超过 5 个改写问题，不要为了凑满数量而输出冗余问题。\n",
    "\n",
    "    【输出格式】只输出 JSON ，不要给出多余内容\n",
    "    ```json\n",
    "    {{\n",
    "    \"query\": [\"改写问题1\", \"改写问题2\"...]\n",
    "    }}```\n",
    "    \"\"\"\n",
    ")\n",
    "\n",
    "query = \"1675 年时，英格兰有多少家咖啡馆？\"\n",
    "conversation_str = f\"user:\\n{query}\\n\"\n",
    "search_info_input = QUERY_REWRITE_PROMPT.format(\n",
    "    TIMESTAMP=datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n",
    "    CONVERSATION=conversation_str\n",
    ")\n",
    "print(search_info_input)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "调用模型接口进行判断。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'id': 'chatcmpl-1a50b468-f25f-4d48-91d8-ff48c86e86a1', 'choices': [{'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'message': {'content': '```json\\n{\\n    \"query\": [\"1675年 英格兰咖啡馆数量\"]\\n}\\n```</s></s>', 'refusal': None, 'role': 'assistant', 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None, 'reasoning_content': None}}], 'created': 1750834480, 'model': 'default', 'object': 'chat.completion', 'service_tier': None, 'system_fingerprint': None, 'usage': {'completion_tokens': 26, 'prompt_tokens': 330, 'total_tokens': 356, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}}\n"
     ]
    }
   ],
   "source": [
    "judge_search_messages = [{\"role\": \"user\", \"content\": search_info_input}]\n",
    "\n",
    "client = OpenAI(base_url=host_url, api_key=model_api_key)\n",
    "search_info_res = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=judge_search_messages\n",
    ")\n",
    "\n",
    "search_info_res = search_info_res.model_dump()\n",
    "print(search_info_res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "将模型回答解析为json格式。\n",
    "- `query`: 需要检索的问题列表。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'query': ['1675年 英格兰咖啡馆数量']}\n"
     ]
    }
   ],
   "source": [
    "import re\n",
    "\n",
    "search_info_res = search_info_res[\"choices\"][0][\"message\"][\"content\"]\n",
    "json_match = re.search(r'```json\\n(.*?)\\n```', search_info_res, re.DOTALL)\n",
    "json_str = json_match.group(1)\n",
    "search_info_res = json.loads(json_str)\n",
    "\n",
    "print(search_info_res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.2. 知识库检索\n",
    "\n",
    "如果上一步判断需要联网搜索，则从知识库中检索相关文本。\n",
    "\n",
    "检索流程如下：\n",
    "\n",
    "1. 查询检索：对每个查询进行向量化处理，使用FAISS索引检索相似度最高的`top_k`个结果，收集所有结果的索引位置到统一列表。\n",
    "\n",
    "2. 结果去重：对收集到的索引进行去重和排序操作，消除重复检索结果，为后续处理准备有序的索引序列。\n",
    "\n",
    "3. 上下文扩展：针对每个目标索引，动态确定其所属文件的边界范围，在文件边界限制内扩展上下文窗口（±context_size），生成包含上下文的新索引集合。\n",
    "\n",
    "4. 连续块合并：将扩展后的索引按物理位置排序，检测连续且同属一个文件的索引序列，将其分组为连续的文本块单元。\n",
    "\n",
    "5. 结果生成：合并块内所有文本内容，最终输出带结构化标记的完整文本。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Retrieved indices: [3, 2, 4]\n",
      "Unique indices after deduplication: [2, 3, 4]\n",
      "Merged chunk range: 0-6\n",
      "\n",
      "段落1:\n",
      "咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，\n",
      "公平贸易咖啡与有机咖啡是一个不断扩大的市场。\n",
      "传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。\n",
      "咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。\n",
      "同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\n",
      "1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\n",
      "1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。\n",
      "18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\n",
      "在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\n",
      "咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。\n",
      "香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\n",
      "不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：\n",
      "土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\n",
      "浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\n",
      "拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "def search_with_context(query_list: list, context_size: int=2,) -> str:\n",
    "    # 步骤1：为每个查询问题检索top_k结果并记录所有检索得到文本索引\n",
    "    all_indices = []\n",
    "    for query in query_list:\n",
    "        query_vector = np.array([embed_fn(query)]).astype('float32')\n",
    "        _, indices = index.search(query_vector, top_k)\n",
    "        all_indices.extend(indices[0].tolist())\n",
    "\n",
    "    # 步骤2: 去除重复的文本索引\n",
    "    unique_indices = sorted(set(all_indices))\n",
    "    print(f\"Retrieved indices: {all_indices}\")\n",
    "    print(f\"Unique indices after deduplication: {unique_indices}\")\n",
    "\n",
    "    # 步骤3：为每个检索结果扩展上下文（相同文件内）\n",
    "    expanded_indices = set()\n",
    "    file_boundaries = {}  # {file_md5: (start_idx, end_idx)}\n",
    "    for target_idx in unique_indices:\n",
    "        target_chunk = text_db[\"chunks\"][target_idx]\n",
    "        target_file_md5 = target_chunk[\"file_md5\"]\n",
    "\n",
    "        if target_file_md5 not in file_boundaries:\n",
    "            file_start = target_idx\n",
    "            while file_start > 0 and text_db[\"chunks\"][file_start - 1][\"file_md5\"] == target_file_md5:\n",
    "                file_start -= 1\n",
    "            file_end = target_idx\n",
    "            while (file_end < len(text_db[\"chunks\"]) - 1 and\n",
    "                text_db[\"chunks\"][file_end + 1][\"file_md5\"] == target_file_md5):\n",
    "                file_end += 1\n",
    "        else:\n",
    "            file_start, file_end = file_boundaries[target_file_md5]\n",
    "\n",
    "        # 计算文件文本块的索引边界\n",
    "        start = max(file_start, target_idx - context_size)\n",
    "        end = min(file_end, target_idx + context_size)\n",
    "\n",
    "        for pos in range(start, end + 1):\n",
    "            expanded_indices.add(pos)\n",
    "\n",
    "    # 步骤4: 排序后合并连续文本块\n",
    "    sorted_indices = sorted(expanded_indices)\n",
    "    groups = []\n",
    "    current_group = [sorted_indices[0]]\n",
    "    for i in range(1, len(sorted_indices)):\n",
    "        if (sorted_indices[i] == sorted_indices[i-1] + 1 and\n",
    "            text_db[\"chunks\"][sorted_indices[i]][\"file_md5\"] ==\n",
    "            text_db[\"chunks\"][sorted_indices[i-1]][\"file_md5\"]):\n",
    "            current_group.append(sorted_indices[i])\n",
    "        else:\n",
    "            groups.append(current_group)\n",
    "            current_group = [sorted_indices[i]]\n",
    "    groups.append(current_group)\n",
    "\n",
    "    # 步骤5: 整理输出结果\n",
    "    result = \"\"\n",
    "    for idx, group in enumerate(groups):\n",
    "        result += f\"\\n段落{idx + 1}:\\n\"\n",
    "        for idx in group:\n",
    "            result += text_db[\"chunks\"][idx][\"text\"] + \"\\n\"\n",
    "        print(f\"Merged chunk range: {group[0]}-{group[-1]}\")\n",
    "\n",
    "    return result\n",
    "\n",
    "relevant_passages = \"\"\n",
    "if search_info_res.get(\"query\", []):\n",
    "    relevant_passages = search_with_context(search_info_res[\"query\"])\n",
    "print(relevant_passages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.3. 生成最终回答\n",
    "#### 4.3.1. 模型输入\n",
    "模型输入为一个消息列表，表示对话的上下文历史。每条消息是一个字典，包含以下字段：\n",
    "- `role`: 表示消息发送者的角色，可以是：\n",
    "    - `user`: 用户消息，表示用户的输入\n",
    "    - `assistant`: 模型消息，表示模型的回复\n",
    "- `content`: 具体文本内容\n",
    "\n",
    "检索增强输入具有以下特性：\n",
    "- 知识库检索：将检索结果拼接到`ANSWER_PROMPT`，作为上下文提供给大模型。\n",
    "- 多轮对话：支持保留历史对话上下文"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "id": "mlpDRG3cVvQE"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'role': 'user', 'content': \"你是阅读理解问答专家。\\n\\n【文档知识】\\n\\n段落1:\\n咖啡（英语：coffee）是指咖啡植物的种子即咖啡豆在经过烘焙磨粉后通过冲泡制成的饮料，是世界上流行范围最为广泛的饮料之一。咖啡在人类饮食中一般为日常的饮品，人们通常会为了提振精神，或在用餐和社交、阅读时饮用。咖啡原产于非洲东岸的埃塞俄比亚，咖啡起源于15-16世纪，从也门被传播至穆斯林世界，16世纪的威尼斯商人将咖啡引入意大利，随后17-18世纪由于欧洲对咖啡的需求，促使殖民者将咖啡树传播并栽种到美洲、东南亚和印度等热带地区，现今有超过70个国家种植咖啡树。未经烘焙的 咖啡生豆作为世界上最大的出口农产品，以及世界上交易量为广泛的热带农产品之一，也是发展中国家出口中最有价值的商品之一。采收的成熟咖啡果会经过剥离果肉的初步加工，再经过烘焙的工序，而成为能制作咖啡的咖啡豆。透过不同的冲泡方式与成分比例，咖啡有浓缩咖啡、卡布奇诺和拿铁咖啡等变化。咖啡豆的品种可大致分为两种：最为普遍的小果咖啡（阿拉比卡），以及颗粒较粗且酸味较低而苦味较浓的中果咖啡（罗布斯塔）。一些争议指咖啡的种植与它环境影响有关，例如肯亚咖啡豆在移植种植后失去了独有的肯亚酸，而肯亚的原种地土壤含有较高浓度的磷酸。因此，\\n公平贸易咖啡与有机咖啡是一个不断扩大的市场。\\n传说9世纪的埃塞俄比亚的牧羊人发现并咀嚼了咖啡果实，随后将咖啡果实带给了附近修道院的僧侣，但僧侣起初不愿食用果实，并把果实扔进火里，经过火烤的咖啡果中冒出香气引来僧侣前来查看，僧侣从余烬中捞出咖啡豆，并将其磨碎溶解在热水中，这才制成了世界上第一杯咖啡。但此故事截至1671年并没有得到任何记载，因此可能是杜撰的。亦有研究认为最初栽培的咖啡源自埃塞俄比亚的哈勒尔。埃塞俄比亚的阿克苏姆王国兴盛时曾一度占据也门南部，6世纪中期，萨珊帝国攻占也门后将阿克苏姆赶出南阿拉伯半岛，可以肯定的是咖啡是从埃塞俄比亚传播到也门的。\\n咖啡传播到穆斯林世界后伊斯兰医学认可了咖啡的好处，认为其可以提振精神并防止酒和大麻对穆斯林的诱惑，15世纪的也门苏菲派修道院在祈祷时使用咖啡来帮助集中注意力。 16世纪初咖啡从也门的摩卡港传播到埃及，随后咖啡馆还出现在叙利亚阿勒颇，并于1554年在奥斯曼帝国首都伊斯坦布尔开业。1511年，由于也门麦加的宗教领袖认为咖啡具有刺激作用，便开始禁止穆斯林饮用咖啡，造成其余阿拉伯世界的苏丹和宗教领袖也相继效仿；其中两位奥斯曼帝国苏丹更是同样出于政治考量，而在1517年和1623年两度禁止咖啡。\\n同样在16世纪，与阿拉伯世界的贸易令威尼斯获得了包括咖啡在内的非洲商品，威尼斯商人则向威尼斯的上流阶级高价推销咖啡。起初意大利的宗教人士对咖啡这种穆斯林饮料持怀疑态度，并称咖啡为“撒旦的苦涩发明（bitter invention of Satan）”或是“阿拉伯酒（wine of Araby）”，1600年，教宗克莱孟八世对咖啡的争议作出裁决，在教宗品尝咖啡后认为可以饮用，并祝福了咖啡。 1616年，荷兰商人彼得·范登布罗克从也门摩卡获得了一些阿拉比卡咖啡树苗并带回了阿姆斯特丹，还在当地植物园种植成功。1658年，荷兰人首先在其殖民地锡兰和印度南部开始种植咖啡，但出于避免供应过剩而降低价格的考量，最终放弃了在锡兰种植，专注于爪哇和苏里南的种植园。\\n1675年时，英格兰就有3000多家咖啡馆；启蒙运动时期，咖啡馆成为民众深入讨论宗教和政治的聚集地，1670年代的英国国王查理二世就曾试图取缔咖啡馆。这一时期的英国人认为咖啡具有药用价值，甚至名医也会推荐将咖啡用于医疗。\\n1773年，波士顿倾茶事件后约翰·亚当斯和许多美国人认为喝茶是不爱国的，令大量美国人在美国独立战争期间改喝咖啡。\\n18世纪，葡萄牙人首先在巴西里约热内卢附近，后来则是圣保罗种植咖啡并建设种植园。1852-1950年，巴西主导了世界咖啡生产，其出口的咖啡比世界其他地区的总和还多。1950年以来，由于哥伦比亚和越南等主要生产国相继出现，而越南在1999年超过哥伦比亚成为世界第二大咖啡生产国，并在2011年达到15%的市场份额，而同年巴西的市场份额仅占33%。\\n在咖啡的原产地埃塞俄比亚，18世纪前咖啡曾被埃塞俄比亚正教会所禁止，直至19世纪后期叶埃塞俄比亚皇帝孟尼利克二世的统治时期才有所开放。\\n咖啡在19世纪中已经引入中国上海，1843年—44年上海对外贸易文献就有记载“枷榧豆5包，每包70斤”，表明当时上海已经从外国进口咖啡豆。\\n香港英文报章在1866年刊登关于coffee shop的报导。1885年香港中文报章以“咖啡”为中文名，此后逐渐成为华语地区普及使用的中文译名。香港在1840年代起有英国人聚居，由于饮食文化的差异，最初被输入到香港的咖啡豆是主要供应西方人饮用，而一般本地华人则不喜欢咖啡苦涩的味道，在早年的香港常有大量从事搬运工作的苦力在码头聚集，为来港的货轮搬运货物，从事体力劳动的苦力比一般华人更容易接触到刚循海路进口的咖啡豆，所以在华人社会中最早有饮用咖啡习惯的群体，却是社会地位低下的码头搬运工。\\n不同地区和民族之间的口味偏好，令咖啡冲泡方式以及调味品的使用多种多样，通常热咖啡添加砂糖、牛奶、奶油、奶精等调味，冷饮咖啡则有更多选择，如酒、薄荷、丁香、柠檬汁等。而不同冲泡和调味方式亦产生出了许多咖啡品类：\\n土耳其咖啡：是种具有古老历史的咖啡饮品和冲泡方式，而土耳其以外的中东国家以及东南欧皆有流行过此种冲泡方式。土耳其咖啡冲泡好后未经过滤即可直接饮用，土耳其传统上会将土耳其咖啡倒入小瓷杯中慢慢啜饮，而处于悬浊状的咖啡残留有少量咖啡渣亦成为土耳其咖啡独特风味与口感的来源。冲泡土耳其咖啡的方法为将咖啡豆研磨成粉末后装入土耳其壶中，倒入热水并与咖啡末搅拌均匀，再加人豆蔻粉充分搅拌，对土耳其壶加热并充分搅拌。咖啡煮至冒泡后停止加热，待泡沫消失，此时可短暂重复加热2次；或是将三分之一的咖啡先倒入到各个杯子中，壶中剩余的咖啡则再度加热，直到沸腾后倒入之前的杯子里。\\n浓缩咖啡：是一种通过迫使接近沸腾的高压水流通过咖啡末制作而成的咖啡，拿铁咖啡和卡布奇诺、玛琪雅朵等皆是以浓缩咖啡为基本制成的。\\n拿铁咖啡：拿铁咖啡是由浓缩咖啡和热牛奶以1:2的比例冲泡，并加入些许奶泡制成的。也可依需求加上两份浓缩咖啡，意大利语称之为“Double”。\\n\\n\\n你的任务是根据对话内容，理解用户需求，参考文档知识回答用户问题，知识参考详细原则如下：\\n- 对于同一信息点，如文档知识与模型通用知识均可支撑，应优先以文档知识为主，并对信息进行验证和综合。\\n- 如果文档知识不足或信息冲突，必须指出“根据资料无法确定”或“不同资料存在矛盾”，不得引入文档知识与通识之外的主观推测。\\n\\n同时，回答问题需要综合考虑规则要求中的各项内容，详细要求如下：\\n【规则要求】\\n* 回答问题时，应优先参考与问题紧密相关的文档知识，不要在答案中引入任何与问题无关的文档内容。\\n* 回答中不可以让用户知道你查询了相关文档。\\n* 回复答案不要出现'根据文档知识'，'根据当前时间'等表述。\\n* 论述突出重点内容，以分点条理清晰的结构化格式输出。\\n\\n【当前时间】\\n2025-06-25 14:54:51\\n\\n【对话内容】\\nuser:\\n1675 年时，英格兰有多少家咖啡馆？\\n\\n\\n直接输出回复内容即可。\\n\"}]\n"
     ]
    }
   ],
   "source": [
    "ANSWER_PROMPT = textwrap.dedent(\n",
    "    \"\"\"\\\n",
    "    你是阅读理解问答专家。\n",
    "\n",
    "    【文档知识】\n",
    "    {DOC_CONTENT}\n",
    "\n",
    "    你的任务是根据对话内容，理解用户需求，参考文档知识回答用户问题，知识参考详细原则如下：\n",
    "    - 对于同一信息点，如文档知识与模型通用知识均可支撑，应优先以文档知识为主，并对信息进行验证和综合。\n",
    "    - 如果文档知识不足或信息冲突，必须指出“根据资料无法确定”或“不同资料存在矛盾”，不得引入文档知识与通识之外的主观推测。\n",
    "\n",
    "    同时，回答问题需要综合考虑规则要求中的各项内容，详细要求如下：\n",
    "    【规则要求】\n",
    "    * 回答问题时，应优先参考与问题紧密相关的文档知识，不要在答案中引入任何与问题无关的文档内容。\n",
    "    * 回答中不可以让用户知道你查询了相关文档。\n",
    "    * 回复答案不要出现'根据文档知识'，'根据当前时间'等表述。\n",
    "    * 论述突出重点内容，以分点条理清晰的结构化格式输出。\n",
    "\n",
    "    【当前时间】\n",
    "    {TIMESTAMP}\n",
    "\n",
    "    【对话内容】\n",
    "    {CONVERSATION}\n",
    "\n",
    "    直接输出回复内容即可。\n",
    "    \"\"\"\n",
    ")\n",
    "\n",
    "if search_info_res.get(\"query\", []):\n",
    "    input = ANSWER_PROMPT.format(\n",
    "        DOC_CONTENT=relevant_passages,\n",
    "        TIMESTAMP=datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"),\n",
    "        CONVERSATION=conversation_str\n",
    "    )\n",
    "else:\n",
    "    input = query\n",
    "\n",
    "messages = [{\"role\": \"user\", \"content\": input}]\n",
    "print(messages)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "qmdYdoIHcEc_"
   },
   "source": [
    "#### 4.3.2. 非流式请求\n",
    "##### 请求模型\n",
    "向API发送请求时，需要考虑以下主要参数：\n",
    "- `messages`（必须）: 对话消息列表\n",
    "- `max_tokens`（可选）: 最大生成token数\n",
    "- `temperature`（可选）: 生成结果的随机性控制\n",
    "- `top_p`（可选）: 核采样参数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "id": "m30avD9cfQQ-"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'id': 'chatcmpl-546e2fc9-9ec1-4d3d-bd2b-c800bb616556', 'choices': [{'finish_reason': 'stop', 'index': 0, 'logprobs': None, 'message': {'content': '1675年时，英格兰有3000多家咖啡馆。</s></s>', 'refusal': None, 'role': 'assistant', 'annotations': None, 'audio': None, 'function_call': None, 'tool_calls': None, 'reasoning_content': None}}], 'created': 1750763172, 'model': 'default', 'object': 'chat.completion', 'service_tier': None, 'system_fingerprint': None, 'usage': {'completion_tokens': 18, 'prompt_tokens': 1978, 'total_tokens': 1996, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}}\n"
     ]
    }
   ],
   "source": [
    "client = OpenAI(base_url=host_url, api_key=model_api_key)\n",
    "response = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=messages,\n",
    "    temperature=1.0,\n",
    "    max_tokens=2048,\n",
    "    top_p=0.7\n",
    ")\n",
    "response = response.model_dump()\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### 模型输出\n",
    "- `content`：最终生成回答"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "id": "COBhn6J9S_xI"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1675年时，英格兰有3000多家咖啡馆。</s></s>\n"
     ]
    }
   ],
   "source": [
    "content = response[\"choices\"][0][\"message\"][\"content\"]\n",
    "print(content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 4.3.3. 流式请求\n",
    "##### 请求模型\n",
    "向API发送请求时，需要考虑以下主要参数：\n",
    "- `messages`（必须）: 对话消息列表\n",
    "- `max_tokens`（可选）: 最大生成token数\n",
    "- `temperature`（可选）: 生成结果的随机性控制\n",
    "- `top_p`（可选）: 核采样参数\n",
    "- `stream`（可选）: 是否流式返回"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[{'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '', 'function_call': None, 'refusal': None, 'role': 'assistant', 'tool_calls': None, 'reasoning_content': ''}, 'finish_reason': None, 'index': 0, 'logprobs': None}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}, {'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '1', 'function_call': None, 'refusal': None, 'role': None, 'tool_calls': None, 'token_ids': [4], 'reasoning_content': None}, 'finish_reason': None, 'index': 0, 'logprobs': None, 'arrival_time': 0.12418651580810547}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}, {'id': 'chatcmpl-bf801edc-abce-496a-8329-0994ec9c1a5b', 'choices': [{'delta': {'content': '6', 'function_call': None, 'refusal': None, 'role': None, 'tool_calls': None, 'token_ids': [9], 'reasoning_content': None}, 'finish_reason': None, 'index': 0, 'logprobs': None, 'arrival_time': 0.14658284187316895}], 'created': 1750411440, 'model': 'default', 'object': 'chat.completion.chunk', 'service_tier': None, 'system_fingerprint': None, 'usage': None}]\n"
     ]
    }
   ],
   "source": [
    "response = client.chat.completions.create(\n",
    "    model=\"default\",\n",
    "    messages=messages,\n",
    "    temperature=1.0,\n",
    "    max_tokens=2048,\n",
    "    top_p=0.7,\n",
    "    stream=True\n",
    ")\n",
    "response_stream = []\n",
    "for chunk in response:\n",
    "    if not chunk.choices:\n",
    "        continue\n",
    "    response_stream.append(chunk.model_dump())\n",
    "\n",
    "print(response_stream[:3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### 模型输出\n",
    "- `content`：最终生成回答"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1675年时，英格兰有3000多家咖啡馆。</s>\n"
     ]
    }
   ],
   "source": [
    "content = \"\"\n",
    "for res in response_stream:\n",
    "    content += res[\"choices\"][0][\"delta\"][\"content\"]\n",
    "print(content)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "colab": {
   "name": "Talk_to_documents_with_embeddings.ipynb",
   "toc_visible": true
  },
  "google": {
   "image_path": "/site-assets/images/share.png",
   "keywords": [
    "examples",
    "googleai",
    "samplecode",
    "python",
    "embed"
   ]
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
